let 和 const


1.let
  • 基本用法

    1
    2
    3
    4
    5
    6
    7
    for(let i =0;i < 3;i++){
    let i = 'abc';
    console.log(i);
    }
    //abc
    //abc
    //abc

    ​ for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。上述代码表明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域。


  • 不存在变量提升

    1
    2
    3
    4
    5
    6
    7
    //var 的情况
    console.log(foo);//undefined
    var foo=2;

    //let 的情况
    console.log(bar);//ReferenceError: foo is not defined
    let bar=2;

    ​ 上述代码表明,使用 var 声明变量,会发生变量提升,即在脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出 undefined。使用 let 声明变量,不会发生变量提升。这表示在声明它之前,变量 bar 是不存在的,这时如果用到它,就会抛出一个错误。


  • 暂时性死区

    ​ 只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

    1
    2
    3
    4
    5
    6
    var tmp=123;

    if(true){
    tmp='abc';//ReferenceError
    let tmp;
    }

    ​ 上述代码中,存在全局变量tmp,但是在块级作用域内 let 又声明了一个局部变量 tmp,导致后者绑定这个块级作用域,不受外部影响,所以在 let 声明变量前,对tmp 赋值会报错。

    ​ 再举一个更详细的例子,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if (true) {
    // Temporal dead zone (TDZ) 开始
    tmp = 'abc'; // ReferenceError
    console.log(tmp); // ReferenceError

    let tmp; // TDZ结束
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
    }

    ​ 上述代码中,在 let 命令声明变量 tmp 前,都属于变量 tmp 的死区。

​ “暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作。

1
2
3
typeof x; // ReferenceError
let x;
typeof undeclared_variable // "undefined"

​ 上述代码通过比较,表明在没有 let 之前,typeof 运算符是百分之百安全的,永远不会报错。使用 let 声明变量,在变量没有声明之前就使用,会抛出一个ReferenceError的错误。

1
2
3
4
5
6
7
8
9
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错

function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]

​ 上述代码报错,也是因为死区,不过这个死区比较隐蔽。参数 x 默认值等于另一个参数 y ,而此时 y 还没有声明,属于”死区“。如果 y 的默认值是 x,就不会报错,因为此时 x 已经声明了。

​ 另外,下面的代码也会报错

1
2
3
4
5
6
// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined

​ 报错原因是在变量 x 还没有被 let 声明完成就使用,属于暂时性死区的问题。


  • 块级作用域和函数声明

    ES6 规定,块级作用域之中,函数声明语句的行为类似于 let ,在块级作用域之外不可引用。

    下面看一段代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function f() { console.log('I am outside!'); }

    (function () {
    if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
    }

    f();
    }());

    ​ 上面代码在 ES5 中运行,会得到“I am inside!”,因为在 if 内声明的函数 f 会被提升到函数头部,实际运行的代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ES5 环境
    function f() { console.log('I am outside!'); }

    (function () {
    function f() { console.log('I am inside!'); }
    if (false) {
    }
    f();
    }());

    ​ 在ES6中运行,结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 浏览器的 ES6 环境
    function f() { console.log('I am outside!'); }

    (function () {
    if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
    }

    f();
    }());
    // Uncaught TypeError: f is not a function

    ​ 理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于 let ,对作用域之外没有影响。但是报错了,这是为什么呢?

    ​ 原因是如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

    • 允许在块级作用域内声明函数。

    • 函数声明类似于 var,即会提升到全局作用域或函数作用域的头部。

    • 同时,函数声明还会提升到所在的块级作用域的头部。

    ​ 根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于 var 声明的变量。上述代码实际运行如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 浏览器的 ES6 环境
    function f() { console.log('I am outside!'); }
    (function () {
    var f = undefined;
    if (false) {
    function f() { console.log('I am inside!'); }
    }

    f();
    }());
    // Uncaught TypeError: f is not a function

    ​ 考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 函数声明语句
    {
    let a = 'secret';
    function f() {
    return a;
    }
    }

    // 函数表达式
    {
    let a = 'secret';
    let f = function () {
    return a;
    };
    }

    2.const

    ​ const 和 let 一样不发生变量提升和存在暂时性死区的问题。

  • 本质

    const 实际上保证的,并不是变量的值不得改动,而是变量指向的内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的内存地址中,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针。const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const PI = 3.1415;
    PI = 3;
    // TypeError: Assignment to constant variable.

    const foo = {};
    // 为 foo 添加一个属性,可以成功
    foo.prop = 123;
    foo.prop // 123
    // 将 foo 指向另一个对象,就会报错
    foo = {}; // TypeError: "foo" is read-only

    ​ 上面代码中,常量 foo 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 foo 指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。